package springiesSimulator;

import element.Mass;
import java.awt.Dimension;
import java.awt.event.KeyEvent;
import java.awt.geom.Point2D;
import util.EnvironmentFactory;
import util.KeyboardInput;
import util.Vector;


/**
 * @author Mark Govea and Mark Hoffman
 */
public class EnvironmentManager {
    private static final int UP = 0;
    private static final int RIGHT = 1;
    private static final int DOWN = 2;
    private static final int LEFT = 3;
    private static final int NUMBER_DIRECTIONS = 4;
    private static final int BUFFER_SIZE = 10;
    private static final int VISCOSITY_SCALE = 100;
    private double myGravityMagnitude;
    private double myGravityDirection;
    private double myViscosity;
    private double myCenterMagnitude;
    private double myCenterExponent;
    private double[] myWallsMagnitude;
    private double[] myWallsMagnitudeFinal;
    private double[] myWallsExponent;
    private EnvironmentFactory myEnvironment;
    private Canvas myCanvas;
    private int myEdgePadding;
    private int[] myWallLocations;

    /**
     * The environmental conditions manager is designed to take environmental
     * conditions and apply them to masses.
     * 
     * @param canvas the pased in canvas
     * @param environment used to get the environment data
     */
    public EnvironmentManager (Canvas canvas, EnvironmentFactory environment) {
        myCanvas = canvas;
        myEnvironment = environment;
        myGravityMagnitude = myEnvironment.getGravity()[1];
        myGravityDirection = myEnvironment.getGravity()[0];
        myViscosity = myEnvironment.getViscosity();
        myCenterMagnitude = myEnvironment.getCenterMass()[0];
        myCenterExponent = myEnvironment.getCenterMass()[1];
        getWallRepelValues();
        setMyEdgePadding(0);
        myWallLocations = new int[EnvironmentFactory.NUMBER_WALLS];
        updateMyWallLocations();
    }

    /**
     * Acts on a mass with all of the current environmental conditions.
     * 
     * @param mass on which to apply the forces
     */
    public void applyEnvironmentalForces (Mass mass) {
        applyGravity(mass);
        applyViscosity(mass);
        applyCenterOfGravity(mass);
        applyRepulsion(mass);
    }

    // In charge of applying gravity
    private void applyGravity (Mass mass) {
        Vector f = new Vector(myGravityDirection, myGravityMagnitude);
        mass.applyForce(f);
    }

    private void applyViscosity (Mass mass) {
        double scale = myViscosity / VISCOSITY_SCALE;
        double scaledMagnitude = mass.getVelocity().getMagnitude() * scale;
        Vector f = new Vector(mass.getVelocity().getDirection(),
                scaledMagnitude);
        f.negate();
        mass.applyForce(f);
    }

    private void applyCenterOfGravity (Mass mass) {
        Point2D center = mass.getMyAssembly().getMyCenterOfMass();
        double distance = Vector.distanceBetween(mass.getCenter(), center);
        double magnitude = myCenterMagnitude
                * (1 / (Math.pow(distance, myCenterExponent)));

        Vector centerForce = new Vector(Vector.angleBetween(center,
                mass.getCenter()), magnitude);
        mass.applyForce(centerForce);
    }

    private void applyRepulsion (Mass mass) {
        int[] walls = getMyWallLocations();

        mass.applyForce(new Vector(Vector.ANGLE_DOWN, repelMagnitude(walls,
                mass.getCenter().getY(), UP)));
        mass.applyForce(new Vector(Vector.ANGLE_UP, repelMagnitude(walls,
                mass.getCenter().getY(), DOWN)));
        mass.applyForce(new Vector(Vector.ANGLE_RIGHT, repelMagnitude(walls,
                mass.getCenter().getY(), LEFT)));
        mass.applyForce(new Vector(Vector.ANGLE_LEFT, repelMagnitude(walls,
                mass.getCenter().getY(), RIGHT)));
    }

    private double repelMagnitude (int[] walls, double massLocation, int side) {
        return myWallsMagnitude[side]
                / Math.pow(Math.abs(walls[side] - massLocation),
                        myWallsExponent[side]);

    }

    private void getWallRepelValues () {
        double[][] walls = myEnvironment.getWalls();
        myWallsMagnitude = new double[NUMBER_DIRECTIONS];
        myWallsMagnitudeFinal = new double[NUMBER_DIRECTIONS];
        myWallsExponent = new double[NUMBER_DIRECTIONS];
        for (int i = 0; i < NUMBER_DIRECTIONS; i++) {
            myWallsMagnitude[i] = walls[i][0];
            myWallsMagnitudeFinal[i] = walls[i][0];
            myWallsExponent[i] = walls[i][1];
        }
    }

    /**
     * @param mass the mass being checked for location
     */
    public void checkBounce (Mass mass) {
        int[] walls = getMyWallLocations();

        if (mass.getLeft() < walls[LEFT]) {
            applyBounce(mass, LEFT);
        }
        else if (mass.getRight() > walls[RIGHT]) {
            applyBounce(mass, RIGHT);
        }
        if (mass.getTop() < walls[UP]) {
            applyBounce(mass, UP);
        }
        else if (mass.getBottom() > walls[DOWN]) {
            applyBounce(mass, DOWN);
        }
    }

    private void applyBounce (Mass mass, int dir) {
        int[] walls = getMyWallLocations();

        Vector velocity = mass.getVelocity();

        double x = velocity.getXChange();
        double y = velocity.getYChange();

        if (dir == UP) {
            mass.setCenter(mass.getCenter().getX(), walls[UP]
                    + mass.getSize().height / 2);
            y = Math.abs(y);
        }
        else if (dir == DOWN) {
            mass.setCenter(mass.getCenter().getX(),
                    walls[DOWN] - mass.getSize().height / 2);
            y = -Math.abs(y);
        }
        else if (dir == LEFT) {
            mass.setCenter(walls[LEFT] + mass.getSize().width / 2,
                    mass.getCenter().getY());
            x = Math.abs(x);
        }
        else if (dir == RIGHT) {
            mass.setCenter(walls[RIGHT] - mass.getSize().width / 2,
                    mass.getCenter().getY());
            x = -Math.abs(x);
        }

        mass.setVelocity(Vector.angleBetween(x, y), velocity.getMagnitude());
    }

    /**
     * updates the wall location, including the wall padding when resizing.
     */
    public void updateMyWallLocations () {
        Dimension window = myCanvas.getSize();
        int padding = getMyEdgePadding();

        myWallLocations[UP] = 0 + padding;
        myWallLocations[LEFT] = 0 + padding;
        myWallLocations[DOWN] = window.getSize().height - padding;
        myWallLocations[RIGHT] = window.getSize().width - padding;
    }

    /**
     * @return locations of the walls.
     */
    public int[] getMyWallLocations () {
        return myWallLocations;
    }

    private int getMyEdgePadding () {
        return myEdgePadding;
    }

    private void setMyEdgePadding (int edgePadding) {
        myEdgePadding = edgePadding;
    }

    /**
     * Toggles environment factors based on keyboard input.
     * Also changes the wall size with up/down arrow keys.
     * 
     * @param keyPressed the int value for keyboard inputs
     */
    public void updateEnvironment (int keyPressed) {

        myGravityMagnitude = KeyboardInput.toggle(keyPressed, KeyEvent.VK_G,
                myGravityMagnitude, myEnvironment.getGravity()[1]);
        myViscosity = KeyboardInput.toggle(keyPressed, KeyEvent.VK_V,
                myViscosity, myEnvironment.getViscosity());
        myCenterMagnitude = KeyboardInput.toggle(keyPressed, KeyEvent.VK_M,
                myCenterMagnitude, myEnvironment.getCenterMass()[0]);
        myWallsMagnitude[UP] = KeyboardInput.toggle(keyPressed, KeyEvent.VK_1,
                myWallsMagnitude[UP], myWallsMagnitudeFinal[UP]);
        myWallsMagnitude[RIGHT] = KeyboardInput.toggle(keyPressed,
                KeyEvent.VK_2, myWallsMagnitude[RIGHT],
                myWallsMagnitudeFinal[RIGHT]);
        myWallsMagnitude[DOWN] = KeyboardInput.toggle(keyPressed,
                KeyEvent.VK_3, myWallsMagnitude[DOWN],
                myWallsMagnitudeFinal[DOWN]);
        myWallsMagnitude[LEFT] = KeyboardInput.toggle(keyPressed,
                KeyEvent.VK_4, myWallsMagnitude[LEFT],
                myWallsMagnitudeFinal[LEFT]);
        // Change walled area size
        if (keyPressed == KeyEvent.VK_UP) {
            setMyEdgePadding(getMyEdgePadding() - BUFFER_SIZE);
            updateMyWallLocations();
        }
        else if (keyPressed == KeyEvent.VK_DOWN) {
            setMyEdgePadding(getMyEdgePadding() + BUFFER_SIZE);
            updateMyWallLocations();
        }

        KeyboardInput.printStatus(myGravityMagnitude, myViscosity,
                myCenterMagnitude, myWallsMagnitude[UP],
                myWallsMagnitude[RIGHT], myWallsMagnitude[DOWN],
                myWallsMagnitude[LEFT], myEdgePadding);
    }
}
